package com.spring.aspect;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.spring.annotation.RequiredLog;
import com.spring.pojo.Log;
import com.spring.pojo.User;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;

/**
 * 由Aspect注解描述的类型为一个切面类型，此类型中可以定义
 * 1)切入点(要切入扩展业务逻辑的一些方法集合)
 * 2)通知方法(拓展业务逻辑的实现)
 */
@Aspect
@Component
public class LogAspect {
    private static Logger log= LoggerFactory.getLogger(LogAspect.class);
    /**
     * 通过@Pointcut定义切入点，@annotation是切入点表达式的一种写法，
     * 这里表示基于@RequriedLog注解描述的方法为切入点方法。
     */
    @Pointcut("@annotation(com.spring.annotation.RequiredLog)")
    public void doLog(){}//这个方法内部不用写任何代码，此方法只负责承载注解的定义。

    /**
     * 通过@Around注解描述功能拓展方法，表示我们在此方法内部可以为目标业务方法进行
     * 功能拓展，可以在目标业务执行之前，也可以在目标业务执行之后，添加拓展业务逻辑
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("doLog()")
    public Object doLogAround(ProceedingJoinPoint joinPoint)throws Throwable{
        int status=1;//初始状态
        String error=null;//初始错误信息
        long time=0l;//耗时
        //System.out.println("@Around.Before");
        long t1=System.currentTimeMillis();
        log.info("@Around.Before {}",t1);
        try{
            Object result=joinPoint.proceed();//调用目标业务执行链(可能会有其它切面的拓展业务+目标方法业务)
            //System.out.println("@Around.After");
            long t2=System.currentTimeMillis();
            log.info("@Around.AfterReturing {}",t2);
            time=t2-t1;
            return result;
        }catch (Throwable e){
            long t3=System.currentTimeMillis();
            log.info("@Around.Exception {}",t3);
            time=t3-t1;
            status=0;//表示出错了
            error=e.getMessage();
            throw e;
        }finally{
            saveLog(joinPoint,time,status,error);
        }
    }
    private void saveLog(ProceedingJoinPoint joinPoint,long time,int status,String error) throws NoSuchMethodException, JsonProcessingException {
        //1.获取用户行为日志
        //1.1获取ip地址
        ServletRequestAttributes requestAttributes=
        (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        String ip=request.getRemoteAddr();
        //1.2获取登陆用户
        String username=((User)request.getSession().getAttribute("user")).getUsername();
        //1.3获取操作名
        //1.3.1获取类的字节码对象
        Class<?> targetCls=joinPoint.getTarget().getClass();
        //1.3.2获取方法签名对象
        MethodSignature ms=(MethodSignature) joinPoint.getSignature();
        //1.3.3获取目标方法对象
        Method targetMethod =
                targetCls.getDeclaredMethod(
                        ms.getName(),//方法名
                        ms.getParameterTypes());//参数列表类型
        //1.3.4获取方法上的注解
        RequiredLog requiredLog = targetMethod.getAnnotation(RequiredLog.class);
        //1.3.5获取操作名
        String operation=requiredLog.operation();
        //1.4获取目标方法名(类全名+方法名)
        String method=targetCls.getName()+"."+targetMethod.getName();
        String params=new ObjectMapper().writeValueAsString(joinPoint.getArgs());
        //2.封装用户行为日志
        Log userLog=new Log();
        userLog.setIp(ip);
        userLog.setUsername(username);
        userLog.setCreatedTime(new Date());
        userLog.setOperation(operation);
        userLog.setMethod(method);
        userLog.setParams(params);
        userLog.setStatus(status);
        userLog.setError(error);
        userLog.setTime(time);
        //3.记录用户行为日志(控制台，文件，数据库、消息中间件)
        log.info("user log is {}",userLog);
    }
}
